package com.dmitriyp.movie.implementation;

import java.io.File;
import java.util.LinkedList;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import org.hibernate.sql.JoinType;

import com.dmitriyp.movie.dao.IMovieManager;
import com.dmitriyp.movie.dao.Order;
import com.dmitriyp.movie.dao.Order.Type;
import com.dmitriyp.movie.mapping.Movie;
import com.dmitriyp.movie.mapping.MovieDBConstants;

public class HibernateMovieManger implements IMovieManager {

	private final SessionFactory sessionFactory;

	public HibernateMovieManger(String filePath) {
		Configuration cfg = new Configuration().configure(filePath);
		ServiceRegistry serviceRegistry = new ServiceRegistryBuilder()
				.applySettings(cfg.getProperties()).buildServiceRegistry();
		sessionFactory = cfg.buildSessionFactory(serviceRegistry);
	}

	public HibernateMovieManger(File file) {
		Configuration cfg = new Configuration().configure(file);
		ServiceRegistry serviceRegistry = new ServiceRegistryBuilder()
				.applySettings(cfg.getProperties()).buildServiceRegistry();
		sessionFactory = cfg.buildSessionFactory(serviceRegistry);
	}

	public Movie get(final int movieId) {
		Movie movie = this.<Movie> doWork(new Operation<Movie>() {

			public Movie execute(Session session) {
				Movie movie = (Movie) session.get(Movie.class, movieId);
				
				if(movie == null) return null;
				
				Hibernate.initialize(movie.getStars());
				return (Movie) session.get(Movie.class, movieId);
			}

		});
		return movie;
	}

	public Movie add(final Movie movie) {
		this.<Integer> doWork(new Operation<Integer>() {

			public Integer execute(Session session) {
				Transaction tx = session.beginTransaction();
				if (movie.getReleaseYear() < 1) {
					tx.rollback();
					return -1;
				}
				int id = (Integer) session.save(movie);
				tx.commit();
				return id;
			}

		});
		return movie;
	}

	public List<Movie> addAll(final List<Movie> movies) {
		List<Movie> result = this
				.<List<Movie>> doWork(new Operation<List<Movie>>() {

					public List<Movie> execute(Session session) {
						List<Movie> result = new LinkedList<Movie>();
						Transaction tx = session.beginTransaction();
						for(Movie movie : movies) {
							if (movie.getReleaseYear() < 1) {
								continue;
							}
							session.save(movie);
							result.add(movie);
						}
						tx.commit();
						return result;
					}
				});
		return result;
	}

	public boolean delete(final int movieId) {
		Boolean s = this.<Boolean> doWork(new Operation<Boolean>() {

			public Boolean execute(Session session) {
				Movie movie = get(movieId);
				if (movie == null)
					return false;
				Transaction tx = session.beginTransaction();
				session.delete(movie);
				tx.commit();
				return true;
			}

		});
		return s;
	}

	public boolean delete(final Movie movie) {
		if(movie == null) return false;
		
		Boolean s = this.<Boolean> doWork(new Operation<Boolean>() {

			public Boolean execute(Session session) {
				Transaction tx = session.beginTransaction();
				session.delete(movie);
				tx.commit();
				return true;
			}

		});
		return s;
	}

	public List<Movie> listAll(final Order... orders) {
		List<Movie> movies = this
				.<List<Movie>> doWork(new Operation<List<Movie>>() {

					public List<Movie> execute(Session session) {
						Criteria criteria = session.createCriteria(Movie.class);
						for (Order order : orders) {
							criteria = criteria
									.addOrder(convertToHibernateOrder(order));
						}
						@SuppressWarnings("unchecked")
						List<Movie> movies = (List<Movie>) criteria.list();
						return movies;
					}
				});
		return movies;
	}

	private org.hibernate.criterion.Order convertToHibernateOrder(Order order) {
		return order.getType() == Type.ASC ? org.hibernate.criterion.Order
				.asc(order.getPropertyName()) : org.hibernate.criterion.Order
				.desc(order.getPropertyName());
	}

	public List<Movie> filter(final String title) {
		List<Movie> result = this
				.<List<Movie>> doWork(new Operation<List<Movie>>() {

					@SuppressWarnings("unchecked")
					public List<Movie> execute(Session session) {
						Criteria criteria = session.createCriteria(Movie.class);
						criteria.add(Restrictions.like(MovieDBConstants.MOVIE_TITLE_FIELD,
								title, MatchMode.ANYWHERE));
						return (List<Movie>) criteria.list();
					}

				});
		return result;
	}

	public List<Movie> filterByStar(final String star) {
		List<Movie> result = this
				.<List<Movie>> doWork(new Operation<List<Movie>>() {

					@SuppressWarnings("unchecked")
					public List<Movie> execute(Session session) {
						Criteria criteria = session
								.createCriteria(Movie.class)
								.createAlias(MovieDBConstants.MOVIE_STARS_FIELD, "strs",
										JoinType.LEFT_OUTER_JOIN)
								.add(Restrictions.eq("strs.elements", star));
						return (List<Movie>) criteria.list();
					}

				});
		return result;
	}

	private <T> T doWork(Operation<T> op) {
		Session session = sessionFactory.openSession();
		T result = op.execute(session);
		session.close();
		return result;
	}

	/**
	 * Interface that present abstraction of some job with {@link Session}.
	 * 
	 * @author dmitriy_p
	 * 
	 * @param <T>
	 */
	private interface Operation<T> {

		/**
		 * Caller have to open session before call and close it after.
		 * 
		 * @param session
		 *            - Hibernate session
		 * @return
		 */
		T execute(Session session);

	}

}
